package com.katesoft.scale4j.persistent.model.unified;

import static com.katesoft.scale4j.persistent.utils.DataAccessUtility.initialRevision;
import static com.katesoft.scale4j.persistent.utils.DataAccessUtility.latestModifyRevision;
import static org.hibernate.criterion.Restrictions.eq;
import static org.hibernate.envers.AuditReaderFactory.get;
import static org.springframework.util.ReflectionUtils.COPYABLE_FIELDS;
import static org.springframework.util.ReflectionUtils.doWithFields;
import static org.springframework.util.ReflectionUtils.makeAccessible;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.UUID;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang.exception.NestableRuntimeException;
import org.hibernate.Session;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.Audited;
import org.hibernate.envers.entities.mapper.relation.lazy.proxy.CollectionProxy;
import org.hibernate.envers.entities.mapper.relation.lazy.proxy.MapProxy;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.springframework.util.ReflectionUtils.FieldCallback;

import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.utils.HibernateReflectionUtility;

/**
 * Default implementation of IBO interface.
 * <p/>
 * Clients should extend this class.
 * <p/>
 * default hibernate access type is field access
 * 
 * @author kate2007
 * @see AbstractPersistentEntity
 */
@MappedSuperclass
@Access(AccessType.FIELD)
@Audited
public abstract class BO implements IBO, Cloneable {
   private static final long serialVersionUID = -3433971556160709874L;
   private static final Logger LOGGER = LogFactory.getLogger(BO.class);

   public static final String PROP_GUID = "guid";
   public static final String PROP_UID = "uniqueIdentifier";
   public static final String PROP_VERSION = "version";

   @Column(name = "global_unique_identifier", nullable = false, updatable = false, unique = true)
   private String guid;

   @Version
   @Column(name = "version", nullable = false)
   private Long version;

   public BO() {
      guid = UUID.randomUUID().toString();
      version = (long) 0;
   }

   @Override
   public Class<? extends BO> getPersistentClass() {
      return getClass();
   }

   @Override
   public String getGlobalUniqueIdentifier() {
      return guid;
   }

   @Override
   public long getVersion() {
      return version == null ? 0L : version;
   }

   /**
    * internal setter, clients should not use this method directly.
    * 
    * @param version
    *           new version
    */
   protected void setVersion(long version) {
      this.version = version;
   }

   /**
    * internal setter, clients should not used this method directly.
    * 
    * @param guid
    *           newValue
    */
   protected void setGlobalUniqueIdentifier(String guid) {
      this.guid = guid;
   }

   @Override
   public void cleanBeanProperties() {
      LOGGER.trace("cleaning bean properties, entity = %s", toString());
      doWithFields(getPersistentClass(), new FieldCallback() {
         @Override
         public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
            if (HibernateReflectionUtility.isPersistentField(field)) {
               LOGGER.trace("cleaning %s.%s", getPersistentClass().getName(), field.getName());
               makeAccessible(field);
               field.set(BO.this, null);
            }
         }
      }, COPYABLE_FIELDS);
   }

   @Override
   public boolean attributesLoaded() {
      if (this instanceof HibernateProxy) {
         final LazyInitializer hibernateLazyInitializer = ((HibernateProxy) this)
                  .getHibernateLazyInitializer();
         return !hibernateLazyInitializer.isUninitialized();
      }
      return true;
   }

   /**
    * this method will be executed in hibernate session, never outside session.
    */
   @Override
   public void forceAttributesLoad() {
      if (this instanceof HibernateProxy) {
         LOGGER.debug("forced hibernate lazy loading for entity = %s", toString());
         LazyInitializer initializer = ((HibernateProxy) this).getHibernateLazyInitializer();
         initializer.initialize();
      }
   }

   /**
    * @return detached criteria pre-populated with identifier and version, further criteria
    *         restrictions are expected.
    */
   public DetachedCriteria entityForUpdateCriteria() {
      DetachedCriteria criteria = DetachedCriteria.forClass(getClass());
      criteria.add(eq(PROP_UID, getUniqueIdentifier()));
      criteria.add(eq(PROP_VERSION, getVersion()));
      criteria.add(eq(PROP_GUID, getGlobalUniqueIdentifier()));
      return criteria;
   }

   /**
    * @return clone of this object(pre-populated with unique identifier and version) that can be
    *         used with find-by-example hibernate method.
    */
   public AbstractPersistentEntity entityForUpdateExample() {
      AbstractPersistentEntity clone = (AbstractPersistentEntity) this.clone();
      clone.cleanBeanProperties();
      clone.setUniqueIdentifier(getUniqueIdentifier());
      clone.setVersion(getVersion());
      clone.setGlobalUniqueIdentifier(getGlobalUniqueIdentifier());
      return clone;
   }

   /**
    * looking for creation date of this entity using audit data.
    * 
    * @param session
    *           hibernate session
    * @return creation timestamp
    */
   public Date getCreationTimestamp(Session session) {
      AuditReader auditReader = get(session);
      Number revision = initialRevision(auditReader, getPersistentClass(), getUniqueIdentifier());
      if (revision == null) {
         LOGGER.warn(
                  "unable to find creation_date for entity[class=%s, unique_identifier=%s], make sure you committed transaction",
                  getPersistentClass().getName(), getUniqueIdentifier());
         return null;
      }
      return auditReader.getRevisionDate(revision);
   }

   /**
    * looking for last update timestamp of this entity using audit data.
    * <p/>
    * NOTE: only modify and delete revision types are included into search criteria, so if entity
    * was not modified(updated/deleted) at all, null result will be returned.
    * 
    * @param session
    *           hibernate session
    * @return last update timestamp if available
    */
   public Date getLastModifyTimestamp(Session session) {
      AuditReader auditReader = get(session);
      Number revision = latestModifyRevision(auditReader, getPersistentClass(),
               getUniqueIdentifier());
      if (revision == null) {
         LOGGER.debug(
                  "unable to find last_modify_date for entity[class=%s, unique_identifier=%s], make sure you committed transaction or "
                           + "updated/deleted entity at least one time", getPersistentClass()
                           .getName(), getUniqueIdentifier());
         return null;
      }
      return auditReader.getRevisionDate(revision);
   }

   /**
    * Force initialization of a proxy or persistent collection. This method is similar to
    * {@link org.hibernate.Hibernate#initialize(Object)}, but allows you to initializeProxy
    * historical data as well.
    * <p/>
    * So clients should prefer this method instead of using
    * {@link org.hibernate.Hibernate#initialize(Object)}.
    * 
    * @param proxy
    *           any proxy object
    */
   @SuppressWarnings("rawtypes")
   protected static void initializeProxy(Object proxy) {
      if (proxy == null) {
         return;
      }
      if (proxy instanceof HibernateProxy) {
         ((HibernateProxy) proxy).getHibernateLazyInitializer().initialize();
      } else if (proxy instanceof PersistentCollection) {
         ((PersistentCollection) proxy).forceInitialization();
      }

      if (proxy instanceof CollectionProxy) {
         ((CollectionProxy) proxy).size();
      } else if (proxy instanceof MapProxy) {
         ((MapProxy) proxy).size();
      }
   }

   @Override
   public String reflectionToString() {
      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE, false);
   }

   /**
    * hashCode is generated using global_unique_identifier, unique_identifier, and version fields.
    * 
    * @return hash of this object.
    */
   @Override
   public int hashCode() {
      HashCodeBuilder builder = new HashCodeBuilder();
      //
      builder.append(getUniqueIdentifier());
      builder.append(getVersion());
      builder.append(getGlobalUniqueIdentifier());
      //
      return builder.toHashCode();
   }

   /**
    * by default objects are equals if global_unique_identifier, unique_identifier, and version
    * match.
    * 
    * @param obj
    *           another object of the same class
    * @return true if equals by global_unique_identifier, unique_identifier, and version.
    */
   @Override
   public boolean equals(Object obj) {
      if (this == obj) {
         return true;
      }
      if (obj == null || getClass() != obj.getClass()) {
         return false;
      }
      BO other = (BO) obj;
      EqualsBuilder builder = new EqualsBuilder();
      //
      builder.append(getUniqueIdentifier(), other.getUniqueIdentifier());
      builder.append(getVersion(), other.getVersion());
      builder.append(getGlobalUniqueIdentifier(), other.getGlobalUniqueIdentifier());
      //
      return builder.isEquals();
   }

   @Override
   public BO clone() {
      try {
         LOGGER.debug("cloning this %s", this.toString());
         return (BO) super.clone();
      } catch (final CloneNotSupportedException e) {
         LOGGER.error(e);
         throw new NestableRuntimeException(e);
      }
   }

   /**
    * This method will return short representation of this entity including class,
    * global_unique_identifier information.
    * <p/>
    * NOTE: toString() does not include unique_identifier and version because it can cause session
    * flushing before transaction commit if somebody will try to use unique identifier.
    * 
    * @return entity representation in format [class_name:/global_unique_identifier]
    */
   @Override
   public String toString() {
      return String.format("%s:/%s", getPersistentClass().getName(), getGlobalUniqueIdentifier());
   }

   /**
    * by default nothing should prevent us from removing from cache.
    * <p/>
    * subclasses must add more useful logic(for example they expect that this entity will not be
    * used for next 12 h, in this case they can remove this entity from cache).
    * <p/>
    * From the other hand it is possible that clients are expecting that this entity will be request
    * a lot of time in nearest future(in this case they would like to leave this entity in cache).
    * 
    * @return true
    */
   @Override
   public boolean isCleanable() {
      return true;
   }

   /**
    * set unique identifier. Implementation class can use auto-generated feature or inject
    * 
    * @param uniqueIdentifier
    */
   protected abstract void setUniqueIdentifier(Long uniqueIdentifier);
}
